package com.p7.framework.permission.shiro.session;

import java.io.Serializable;

import com.p7.framework.permission.shiro.utils.SerializeUtils;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * session 缓存，CachingSessionDAO
 * @ClassName: ClientCachingSessionDAO 
 * @author yz
 * @date 2019年2月13日 下午5:07:23
 */
public class ClientCachingSessionDAO extends CachingSessionDAO {

	private static final Logger LOGGER = LoggerFactory.getLogger(ClientCachingSessionDAO.class);

	private SessionDAO redisSessionDAO;

	/**
	 * 删除会话；当会话过期/会话停止（如用户退出时）会调用
	 */
	@Override
	protected void doDelete(Session session) {
		redisSessionDAO.delete(session);
	}

	/**
	 * 如DefaultSessionManager在创建完session后会调用该方法；
	 * 如保存到关系数据库/文件系统/NoSQL数据库；即可以实现会话的持久化；
	 * 返回会话ID；主要此处返回的ID.equals(session.getId())；
	 */
	@Override
	protected Serializable doCreate(Session session) {
		Serializable sessionId = redisSessionDAO.create(session);
		assignSessionId(session, sessionId);
		return sessionId;
	}

	/**
	 * 根据会话ID获取会话
	 *
	 * @param sessionId 会话ID
	 * @return
	 */
	@Override
	protected Session doReadSession(Serializable sessionId) {
		byte[] bytes = SerializeUtils.serialize(redisSessionDAO.readSession(sessionId));
		if (bytes != null && bytes.length > 0) {
			Session session = (Session) SerializeUtils.deserialize(bytes);
			return session;
		}
		return null;
	}

	/**
	 * DefaultSessionManager 及其父类中的方法如：onStop、onExpiration、applyGlobalSessionTimeout、setTimeout、touch、setAttribute、removeAttribute
	 * 都调用了onChange方法，onChange方法中都调用了update方法，update方法的实现是DefaultWebSessionManager属性sessionDAO的实现，
	 * 这里的sessionDAO=clife.sleep.commons.permission.shiro.shiro.session.ClientCachingSessionDAO，即本方法即update的具体实现。
	 * 具体实现内容：
	 * 1.调用doUpdate方法，此方法是子类需要实现的钩子函数，用来实现session的持久化，即clife.sleep.commons.permission.shiro.shiro.session.ClientCachingSessionDAO#doUpdate，
	 *   此方法实现了session 的redis持久化，具体实现：
	 *   1.1校验session是否过期
	 *   1.2校验session是否有属性更改，详见ClientSession
	 *   1.3如果session属性有更改，持久化session到redis，同时更新本地缓存
	 * 2.doUpdate方法结束后，判断本地缓存是否过期，如果过期，将当前session保存到本地缓存，同时更新redis中的session信息（主要是指过redis的过期失效时间）
	 * @param session
	 * @throws UnknownSessionException
	 */
	@Override
	public void update(Session session) throws UnknownSessionException {
		doUpdate(session);

		// 判断本地缓存是否过期，如果过期则添加到本地缓存
		Cache<Serializable, Session> cache = getCache();

		// 判断本地缓存是否过期，如果过期则添加到本地缓存
		if (cache != null) {
			Session cacheSession = cache.get(session.getId());
			if (cacheSession == null) {
				cache.put(session.getId(), session);
				/**
				 * 如果本地缓存已失效，shiro会重新查询redis，将数据缓存在本地缓存中，此时自定义更新一次redis中的缓存，重置失效时间
				 * 会导致的影响：第一次登录时
				 */
				redisSessionDAO.update(session);
			}
		}
	}

	/**
	 * 1.创建会话时，ClientSession的hasChanged方法默认为true，即更新到redis中，同时更新本地缓存
	 * 2.更新会话，如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用此方法，
	 *   但ClientSession继承自SimpleSession，实现当且仅当有属性更改时更新redis，同时更新本地缓存
	 * @Title: doUpdate
	 * @see org.apache.shiro.session.mgt.eis.CachingSessionDAO#doUpdate(org.apache.shiro.session.Session)
	 * @param session
	 * @date 2019年2月16日 下午1:56:07
	 * @author yz
	 */
	@Override
	protected void doUpdate(Session session) {
		if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
			return;
		}

		// 当且仅当session信息具有更改时，触发redis更新和本地缓存的更新
		// 注意，我重写了SimpleSession的一些方法，添加了一些属性，详见clife.sleep.commons.permission.shiro.shiro.session.ClientSession
		if (session instanceof ClientSession) {
			ClientSession ss = (ClientSession) session;
//			if (!ss.getHasChanged()) {
//				LOGGER.info("session信息没有更改或更改的字段不参与redis更新，不更新缓存信息...");
//				return;
//			}
//			// 如果没有返回 证明有调用 setAttribute往redis 放的时候永远设置为false
//			ss.setHasChanged(false);
		}
		redisSessionDAO.update(session);

		// shiro中每次持久化session时都会默认的调用本地缓存一次，这里处理的是触发 redis
		// 更新时才更新cache，以达到cache正常过期的目的，
		// 如果本地缓存失效，那么update方法中重新更新了缓存，并且更新了redis缓存中的失效时间
		cache(session, session.getId());
	}

	/**
	 * 获取本地缓存
	 * @Title: getCache
	 * @return
	 * @date 2019年2月16日 下午1:54:56
	 * @author yz
	 */
	public Cache<Serializable, Session> getCache() {
		Cache<Serializable, Session> cache = super.getActiveSessionsCache();
		if (cache == null) {
			cache = createActiveSessionsCache();
		}
		return cache;
	}

	public SessionDAO getRedisSessionDAO() {
		return redisSessionDAO;
	}

	public void setRedisSessionDAO(SessionDAO redisSessionDAO) {
		this.redisSessionDAO = redisSessionDAO;
	}
}
